﻿function WebGLBindingStates(gl, extensions, attributes, capabilities) {

  const maxVertexAttributes = gl.getParameter(gl.MAX_VERTEX_ATTRIBS);

  const extension = capabilities.isWebGL2 ? null : extensions.get('OES_vertex_array_object');
  const vaoAvailable = capabilities.isWebGL2 || extension !== null;

  const bindingStates = {};

  const defaultState = createBindingState(null);
  let currentState = defaultState;

  function setup(object, material, program, geometry, index) {

    let updateBuffers = false;

    if (vaoAvailable) {

      const state = getBindingState(geometry, program, material);

      if (currentState !== state) {

        currentState = state;
        bindVertexArrayObject(currentState.object);

      }

      updateBuffers = needsUpdate(geometry, index);

      if (updateBuffers) saveCache(geometry, index);

    } else {

      const wireframe = (material.wireframe === true);

      if (currentState.geometry !== geometry.id ||
        currentState.program !== program.id ||
        currentState.wireframe !== wireframe) {

        currentState.geometry = geometry.id;
        currentState.program = program.id;
        currentState.wireframe = wireframe;

        updateBuffers = true;

      }

    }

    if (object.isInstancedMesh === true) {

      updateBuffers = true;

    }

    if (index !== null) {

      attributes.update(index, gl.ELEMENT_ARRAY_BUFFER);

    }

    if (updateBuffers) {

      setupVertexAttributes(object, material, program, geometry);

      if (index !== null) {

        gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, attributes.get(index).buffer);

      }

    }

  }

  function createVertexArrayObject() {

    if (capabilities.isWebGL2) return gl.createVertexArray();

    return extension.createVertexArrayOES();

  }

  function bindVertexArrayObject(vao) {

    if (capabilities.isWebGL2) return gl.bindVertexArray(vao);

    return extension.bindVertexArrayOES(vao);

  }

  function deleteVertexArrayObject(vao) {

    if (capabilities.isWebGL2) return gl.deleteVertexArray(vao);

    return extension.deleteVertexArrayOES(vao);

  }

  function getBindingState(geometry, program, material) {

    const wireframe = (material.wireframe === true);

    let programMap = bindingStates[geometry.id];

    if (programMap === undefined) {

      programMap = {};
      bindingStates[geometry.id] = programMap;

    }

    let stateMap = programMap[program.id];

    if (stateMap === undefined) {

      stateMap = {};
      programMap[program.id] = stateMap;

    }

    let state = stateMap[wireframe];

    if (state === undefined) {

      state = createBindingState(createVertexArrayObject());
      stateMap[wireframe] = state;

    }

    return state;

  }

  function createBindingState(vao) {

    const newAttributes = [];
    const enabledAttributes = [];
    const attributeDivisors = [];

    for (let i = 0; i < maxVertexAttributes; i++) {

      newAttributes[i] = 0;
      enabledAttributes[i] = 0;
      attributeDivisors[i] = 0;

    }

    return {

      // for backward compatibility on non-VAO support browser
      geometry: null,
      program: null,
      wireframe: false,

      newAttributes: newAttributes,
      enabledAttributes: enabledAttributes,
      attributeDivisors: attributeDivisors,
      object: vao,
      attributes: {},
      index: null

    };

  }

  function needsUpdate(geometry, index) {

    const cachedAttributes = currentState.attributes;
    const geometryAttributes = geometry.attributes;

    if (Object.keys(cachedAttributes).length !== Object.keys(geometryAttributes).length) return true;

    for (const key in geometryAttributes) {

      const cachedAttribute = cachedAttributes[key];
      const geometryAttribute = geometryAttributes[key];

      if (cachedAttribute === undefined) return true;

      if (cachedAttribute.attribute !== geometryAttribute) return true;

      if (cachedAttribute.data !== geometryAttribute.data) return true;

    }

    if (currentState.index !== index) return true;

    return false;

  }

  function saveCache(geometry, index) {

    const cache = {};
    const attributes = geometry.attributes;

    for (const key in attributes) {

      const attribute = attributes[key];

      const data = {};
      data.attribute = attribute;

      if (attribute.data) {

        data.data = attribute.data;

      }

      cache[key] = data;

    }

    currentState.attributes = cache;

    currentState.index = index;

  }

  function initAttributes() {

    const newAttributes = currentState.newAttributes;

    for (let i = 0, il = newAttributes.length; i < il; i++) {

      newAttributes[i] = 0;

    }

  }

  function enableAttribute(attribute) {

    enableAttributeAndDivisor(attribute, 0);

  }

  function enableAttributeAndDivisor(attribute, meshPerAttribute) {

    const newAttributes = currentState.newAttributes;
    const enabledAttributes = currentState.enabledAttributes;
    const attributeDivisors = currentState.attributeDivisors;

    newAttributes[attribute] = 1;

    if (enabledAttributes[attribute] === 0) {

      gl.enableVertexAttribArray(attribute);
      enabledAttributes[attribute] = 1;

    }

    if (attributeDivisors[attribute] !== meshPerAttribute) {

      const extension = capabilities.isWebGL2 ? gl : extensions.get('ANGLE_instanced_arrays');

      extension[capabilities.isWebGL2 ? 'vertexAttribDivisor' : 'vertexAttribDivisorANGLE'](attribute, meshPerAttribute);
      attributeDivisors[attribute] = meshPerAttribute;

    }

  }

  function disableUnusedAttributes() {

    const newAttributes = currentState.newAttributes;
    const enabledAttributes = currentState.enabledAttributes;

    for (let i = 0, il = enabledAttributes.length; i < il; i++) {

      if (enabledAttributes[i] !== newAttributes[i]) {

        gl.disableVertexAttribArray(i);
        enabledAttributes[i] = 0;

      }

    }

  }

  function vertexAttribPointer(index, size, type, normalized, stride, offset) {

    if (capabilities.isWebGL2 === true && (type === gl.INT || type === gl.UNSIGNED_INT)) {

      gl.vertexAttribIPointer(index, size, type, stride, offset);

    } else {

      gl.vertexAttribPointer(index, size, type, normalized, stride, offset);

    }

  }

  function setupVertexAttributes(object, material, program, geometry) {

    if (capabilities.isWebGL2 === false && (object.isInstancedMesh || geometry.isInstancedBufferGeometry)) {

      if (extensions.get('ANGLE_instanced_arrays') === null) return;

    }

    initAttributes();

    const geometryAttributes = geometry.attributes;

    const programAttributes = program.getAttributes();

    const materialDefaultAttributeValues = material.defaultAttributeValues;

    for (const name in programAttributes) {

      const programAttribute = programAttributes[name];

      if (programAttribute >= 0) {

        const geometryAttribute = geometryAttributes[name];

        if (geometryAttribute !== undefined) {

          const normalized = geometryAttribute.normalized;
          const size = geometryAttribute.itemSize;

          const attribute = attributes.get(geometryAttribute);

          // TODO Attribute may not be available on context restore

          if (attribute === undefined) continue;

          const buffer = attribute.buffer;
          const type = attribute.type;
          const bytesPerElement = attribute.bytesPerElement;

          if (geometryAttribute.isInterleavedBufferAttribute) {

            const data = geometryAttribute.data;
            const stride = data.stride;
            const offset = geometryAttribute.offset;

            if (data && data.isInstancedInterleavedBuffer) {

              enableAttributeAndDivisor(programAttribute, data.meshPerAttribute);

              if (geometry._maxInstanceCount === undefined) {

                geometry._maxInstanceCount = data.meshPerAttribute * data.count;

              }

            } else {

              enableAttribute(programAttribute);

            }

            gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
            vertexAttribPointer(programAttribute, size, type, normalized, stride * bytesPerElement, offset * bytesPerElement);

          } else {

            if (geometryAttribute.isInstancedBufferAttribute) {

              enableAttributeAndDivisor(programAttribute, geometryAttribute.meshPerAttribute);

              if (geometry._maxInstanceCount === undefined) {

                geometry._maxInstanceCount = geometryAttribute.meshPerAttribute * geometryAttribute.count;

              }

            } else {

              enableAttribute(programAttribute);

            }

            gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
            vertexAttribPointer(programAttribute, size, type, normalized, 0, 0);

          }

        } else if (name === 'instanceMatrix') {

          const attribute = attributes.get(object.instanceMatrix);

          // TODO Attribute may not be available on context restore

          if (attribute === undefined) continue;

          const buffer = attribute.buffer;
          const type = attribute.type;

          enableAttributeAndDivisor(programAttribute + 0, 1);
          enableAttributeAndDivisor(programAttribute + 1, 1);
          enableAttributeAndDivisor(programAttribute + 2, 1);
          enableAttributeAndDivisor(programAttribute + 3, 1);

          gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

          gl.vertexAttribPointer(programAttribute + 0, 4, type, false, 64, 0);
          gl.vertexAttribPointer(programAttribute + 1, 4, type, false, 64, 16);
          gl.vertexAttribPointer(programAttribute + 2, 4, type, false, 64, 32);
          gl.vertexAttribPointer(programAttribute + 3, 4, type, false, 64, 48);

        } else if (name === 'instanceColor') {

          const attribute = attributes.get(object.instanceColor);

          // TODO Attribute may not be available on context restore

          if (attribute === undefined) continue;

          const buffer = attribute.buffer;
          const type = attribute.type;

          enableAttributeAndDivisor(programAttribute, 1);

          gl.bindBuffer(gl.ARRAY_BUFFER, buffer);

          gl.vertexAttribPointer(programAttribute, 3, type, false, 12, 0);

        } else if (materialDefaultAttributeValues !== undefined) {

          const value = materialDefaultAttributeValues[name];

          if (value !== undefined) {

            switch (value.length) {

              case 2:
                gl.vertexAttrib2fv(programAttribute, value);
                break;

              case 3:
                gl.vertexAttrib3fv(programAttribute, value);
                break;

              case 4:
                gl.vertexAttrib4fv(programAttribute, value);
                break;

              default:
                gl.vertexAttrib1fv(programAttribute, value);

            }

          }

        }

      }

    }

    disableUnusedAttributes();

  }

  function dispose() {

    reset();

    for (const geometryId in bindingStates) {

      const programMap = bindingStates[geometryId];

      for (const programId in programMap) {

        const stateMap = programMap[programId];

        for (const wireframe in stateMap) {

          deleteVertexArrayObject(stateMap[wireframe].object);

          delete stateMap[wireframe];

        }

        delete programMap[programId];

      }

      delete bindingStates[geometryId];

    }

  }

  function releaseStatesOfGeometry(geometry) {

    if (bindingStates[geometry.id] === undefined) return;

    const programMap = bindingStates[geometry.id];

    for (const programId in programMap) {

      const stateMap = programMap[programId];

      for (const wireframe in stateMap) {

        deleteVertexArrayObject(stateMap[wireframe].object);

        delete stateMap[wireframe];

      }

      delete programMap[programId];

    }

    delete bindingStates[geometry.id];

  }

  function releaseStatesOfProgram(program) {

    for (const geometryId in bindingStates) {

      const programMap = bindingStates[geometryId];

      if (programMap[program.id] === undefined) continue;

      const stateMap = programMap[program.id];

      for (const wireframe in stateMap) {

        deleteVertexArrayObject(stateMap[wireframe].object);

        delete stateMap[wireframe];

      }

      delete programMap[program.id];

    }

  }

  function reset() {

    resetDefaultState();

    if (currentState === defaultState) return;

    currentState = defaultState;
    bindVertexArrayObject(currentState.object);

  }

  // for backward-compatilibity

  function resetDefaultState() {

    defaultState.geometry = null;
    defaultState.program = null;
    defaultState.wireframe = false;

  }

  return {

    setup: setup,
    reset: reset,
    resetDefaultState: resetDefaultState,
    dispose: dispose,
    releaseStatesOfGeometry: releaseStatesOfGeometry,
    releaseStatesOfProgram: releaseStatesOfProgram,

    initAttributes: initAttributes,
    enableAttribute: enableAttribute,
    disableUnusedAttributes: disableUnusedAttributes

  };

}


export {WebGLBindingStates};
